#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
LimeReader macOS DMG 创建工具
创建专业的macOS DMG安装包

功能:
1. 创建精美的DMG安装包
2. 自动配置窗口大小和布局
3. 添加应用程序文件夹链接
4. 设置自定义图标和背景
5. 支持EULA许可证协议
6. 自动签名和公证（可选）

作者: Wang Zhen
版本: 1.0.0
"""

import os
import sys
import json
import shutil
import tempfile
import subprocess
from pathlib import Path
from typing import Dict, Any, Optional, List
from datetime import datetime
import logging

# 配置日志
logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('dmg_creation.log', encoding='utf-8'),
        logging.StreamHandler()
    ]
)
logger = logging.getLogger(__name__)

class MacOSDMGCreator:
    """macOS DMG创建器"""

    def __init__(self, config_file: str = 'release_config.json'):
        """
        初始化DMG创建器

        Args:
            config_file: 配置文件路径
        """
        self.config_file = config_file
        self.config = self.load_config()
        self.temp_dir = None

    def load_config(self) -> Dict[str, Any]:
        """加载配置文件"""
        try:
            with open(self.config_file, 'r', encoding='utf-8') as f:
                return json.load(f)
        except FileNotFoundError:
            logger.error(f"配置文件不存在: {self.config_file}")
            return self.get_default_config()
        except Exception as e:
            logger.error(f"配置文件加载失败: {e}")
            return self.get_default_config()

    def get_default_config(self) -> Dict[str, Any]:
        """获取默认配置"""
        return {
            "app_info": {
                "name": "LimeReader",
                "chinese_name": "影谷长图处理大师",
                "bundle_id": "com.limereader.desktop"
            },
            "dmg_config": {
                "window_size": {"width": 640, "height": 480},
                "icon_size": 128,
                "background_color": "#ffffff",
                "background_image": "resources/dmg_background.png",
                "show_status_bar": True,
                "show_tab_view": False,
                "show_toolbar": False,
                "show_pathbar": False,
                "show_sidebar": False,
                "arrange_by": "name"
            }
        }

    def create_dmg(self, app_path: Path, version: str, output_dir: Path = None) -> Dict[str, Any]:
        """
        创建DMG文件

        Args:
            app_path: .app文件路径
            version: 版本号
            output_dir: 输出目录

        Returns:
            创建结果字典
        """
        if not app_path.exists() or not app_path.is_dir():
            return {
                'success': False,
                'error': f'应用程序路径无效: {app_path}'
            }

        if not app_path.name.endswith('.app'):
            return {
                'success': False,
                'error': '必须提供.app格式的应用程序'
            }

        logger.info(f"开始创建DMG: {app_path.name} v{version}")

        # 设置输出目录
        if output_dir is None:
            output_dir = Path('dist')
        output_dir.mkdir(exist_ok=True)

        # 创建临时目录
        self.temp_dir = Path(tempfile.mkdtemp(prefix='dmg_creation_'))
        logger.info(f"临时目录: {self.temp_dir}")

        try:
            # DMG文件名
            app_name = self.config['app_info']['name']
            dmg_name = f"{app_name}_v{version}_macOS.dmg"
            dmg_path = output_dir / dmg_name

            # 删除已存在的DMG文件
            if dmg_path.exists():
                dmg_path.unlink()
                logger.info(f"删除已存在的DMG: {dmg_path}")

            # 1. 准备DMG内容
            dmg_content_dir = self.prepare_dmg_content(app_path)

            # 2. 创建DMG
            result = self.create_dmg_image(dmg_content_dir, dmg_path, version)

            if result['success']:
                logger.info(f"DMG创建成功: {dmg_path}")
                result.update({
                    'dmg_path': str(dmg_path),
                    'dmg_name': dmg_name,
                    'dmg_size': dmg_path.stat().st_size,
                    'dmg_size_mb': round(dmg_path.stat().st_size / (1024 * 1024), 2)
                })
            else:
                logger.error(f"DMG创建失败: {result.get('error')}")

            return result

        except Exception as e:
            error_msg = f"DMG创建异常: {e}"
            logger.error(error_msg)
            return {
                'success': False,
                'error': error_msg
            }
        finally:
            # 清理临时目录
            if self.temp_dir and self.temp_dir.exists():
                shutil.rmtree(self.temp_dir)
                logger.info(f"清理临时目录: {self.temp_dir}")

    def prepare_dmg_content(self, app_path: Path) -> Path:
        """准备DMG内容"""
        dmg_content_dir = self.temp_dir / 'dmg_content'
        dmg_content_dir.mkdir(exist_ok=True)

        # 复制应用程序（使用subprocess cp命令保持权限）
        app_dest = dmg_content_dir / app_path.name
        result = subprocess.run([
            'cp', '-R', str(app_path), str(app_dest)
        ], capture_output=True, text=True)

        if result.returncode != 0:
            # 如果cp失败，回退到shutil
            shutil.copytree(app_path, app_dest, copy_function=shutil.copy2)

        logger.info(f"复制应用程序: {app_path.name}")

        # 确保可执行文件有正确的权限
        executable_path = app_dest / 'Contents' / 'MacOS' / self.config['app_info']['name']
        if executable_path.exists():
            # 使用subprocess chmod确保权限正确
            subprocess.run(['chmod', '+x', str(executable_path)], capture_output=True)
            logger.info(f"设置可执行文件权限: {executable_path.name}")

        # 重新签名应用程序（修复权限修改后的签名问题）
        self.resign_application(app_dest)

        # 创建应用程序文件夹链接
        applications_link = dmg_content_dir / 'Applications'
        applications_link.symlink_to('/Applications', target_is_directory=True)
        logger.info("创建Applications文件夹链接")

        # 创建README文件
        readme_content = self.create_readme_content()
        readme_path = dmg_content_dir / 'README.txt'
        with open(readme_path, 'w', encoding='utf-8') as f:
            f.write(readme_content)
        logger.info("创建README.txt文件")

        # 复制背景图（如果存在）
        self.copy_background_image(dmg_content_dir)

        # 设置文件权限
        self.set_file_permissions(dmg_content_dir)

        return dmg_content_dir

    def create_readme_content(self) -> str:
        """创建README内容"""
        app_name = self.config['app_info']['name']
        chinese_name = self.config['app_info']['chinese_name']

        content = f"""{chinese_name} ({app_name}) 安装说明

=====================================

安装方法：
1. 将 {app_name}.app 拖拽到 Applications 文件夹
2. 或者双击 {app_name}.app 直接运行

注意事项：
• 建议从Applications文件夹启动应用
• 首次运行可能需要在系统设置中允许应用运行
• 如遇到安全提示，请在"系统偏好设置 > 安全性与隐私"中允许

更多信息：
• 官网: https://ctdy123.com
• 技术支持: support@ctdy123.com

安装日期: {datetime.now().strftime('%Y年%m月%d日')}
版本: 请查看应用关于信息
"""
        return content

    def copy_background_image(self, dmg_content_dir: Path):
        """复制DMG背景图"""
        dmg_config = self.config.get('dmg_config', {})
        background_image = dmg_config.get('background_image')

        if not background_image:
            logger.info("未配置背景图，跳过")
            return

        # 查找背景图文件
        possible_paths = [
            Path(background_image),  # 相对于当前目录
            Path(__file__).parent / background_image,  # 相对于脚本目录
            Path(__file__).parent.parent / background_image,  # 相对于项目根目录
        ]

        background_src = None
        for path in possible_paths:
            if path.exists():
                background_src = path
                break

        if not background_src:
            logger.warning(f"未找到背景图文件: {background_image}")
            return

        # 创建隐藏的.background目录
        background_dir = dmg_content_dir / '.background'
        background_dir.mkdir(exist_ok=True)

        # 复制背景图
        background_dest = background_dir / 'background.png'
        shutil.copy2(background_src, background_dest)
        logger.info(f"复制背景图: {background_src} -> {background_dest}")

    def resign_application(self, app_path: Path):
        """重新签名应用程序"""
        try:
            logger.info("重新签名应用程序...")

            # 签名所有框架
            frameworks_dir = app_path / 'Contents' / 'Frameworks'
            if frameworks_dir.exists():
                for framework in frameworks_dir.glob('*.framework'):
                    result = subprocess.run([
                        'codesign', '-f', '-s', '-', str(framework)
                    ], capture_output=True, text=True)
                    if result.returncode != 0:
                        logger.warning(f"框架签名失败 {framework.name}: {result.stderr}")

                for dylib in frameworks_dir.glob('*.dylib'):
                    result = subprocess.run([
                        'codesign', '-f', '-s', '-', str(dylib)
                    ], capture_output=True, text=True)
                    if result.returncode != 0:
                        logger.warning(f"库签名失败 {dylib.name}: {result.stderr}")

            # 签名所有插件
            plugins_dir = app_path / 'Contents' / 'PlugIns'
            if plugins_dir.exists():
                for plugin in plugins_dir.rglob('*.dylib'):
                    result = subprocess.run([
                        'codesign', '-f', '-s', '-', str(plugin)
                    ], capture_output=True, text=True)
                    if result.returncode != 0:
                        logger.warning(f"插件签名失败 {plugin.name}: {result.stderr}")

            # 签名主可执行文件
            executable_path = app_path / 'Contents' / 'MacOS' / self.config['app_info']['name']
            if executable_path.exists():
                result = subprocess.run([
                    'codesign', '-f', '-s', '-', str(executable_path)
                ], capture_output=True, text=True)
                if result.returncode != 0:
                    logger.warning(f"可执行文件签名失败: {result.stderr}")

            # 签名整个应用包
            result = subprocess.run([
                'codesign', '-f', '-s', '-', str(app_path)
            ], capture_output=True, text=True)
            if result.returncode == 0:
                logger.info("应用程序重新签名完成")
            else:
                logger.warning(f"应用包签名失败: {result.stderr}")

        except Exception as e:
            logger.warning(f"重新签名异常: {e}")

    def set_file_permissions(self, directory: Path):
        """设置文件权限"""
        for item in directory.rglob('*'):
            try:
                if item.is_file():
                    # 设置文件权限为644
                    item.chmod(0o644)
                elif item.is_dir() and not item.is_symlink():
                    # 设置目录权限为755
                    item.chmod(0o755)
            except Exception as e:
                logger.warning(f"设置权限失败 {item}: {e}")

    def create_dmg_image(self, source_dir: Path, dmg_path: Path, version: str) -> Dict[str, Any]:
        """创建DMG镜像文件"""
        try:
            logger.info("直接创建压缩DMG")

            # 直接创建压缩DMG，跳过临时挂载过程
            result = subprocess.run([
                'hdiutil', 'create',
                '-srcfolder', str(source_dir),
                '-volname', f"{self.config['app_info']['name']} {version}",
                '-fs', 'HFS+',
                '-format', 'UDZO',
                '-imagekey', 'zlib-level=9',
                str(dmg_path)
            ], capture_output=True, text=True)

            if result.returncode != 0:
                return {
                    'success': False,
                    'error': f'创建DMG失败: {result.stderr}'
                }

            logger.info("DMG创建成功")
            return {
                'success': True,
                'message': 'DMG创建成功'
            }

        except Exception as e:
            return {
                'success': False,
                'error': f'创建DMG镜像异常: {e}'
            }

    def configure_dmg_appearance(self, version: str):
        """配置DMG窗口外观"""
        dmg_config = self.config.get('dmg_config', {})

        # 检查是否有背景图
        has_background_image = dmg_config.get('background_image') is not None

        # 创建AppleScript来配置窗口外观
        if has_background_image:
            background_setting = '''
        -- 设置背景图
        set background picture of the icon view options of container window to file ".background:background.png"'''
        else:
            background_setting = '''
        -- 设置背景色（白色）
        set background color of the icon view options of container window to {65535, 65535, 65535}'''

        script_content = f'''
tell application "Finder"
    tell disk "{self.config['app_info']['name']} {version}"
        open
        set current view of container window to icon view
        set toolbar visible of container window to {str(dmg_config.get('show_toolbar', False)).lower()}
        set statusbar visible of container window to {str(dmg_config.get('show_status_bar', True)).lower()}
        set path bar visible of container window to {str(dmg_config.get('show_pathbar', False)).lower()}
        set sidebar width of container window to 0

        -- 设置窗口大小
        set bounds of container window to {{100, 100, {100 + dmg_config.get('window_size', {}).get('width', 640)}, {100 + dmg_config.get('window_size', {}).get('height', 480)}}}

        -- 设置图标大小
        set icon size of the icon view options of container window to {dmg_config.get('icon_size', 128)}

        -- 设置排列方式
        set arrangement of the icon view options of container window to not arranged
{background_setting}

        -- 居中显示图标
        set position of item "{self.config['app_info']['name']}.app" of container window to {{250, 300}}
        set position of item "Applications" of container window to {{450, 300}}
        set position of item "README.txt" of container window to {{250, 150}}

        close
        open
        update without registering applications
        delay 5
    end tell
end tell
'''

        try:
            # 保存脚本到临时文件
            script_file = self.temp_dir / 'configure_dmg.scpt'
            with open(script_file, 'w', encoding='utf-8') as f:
                f.write(script_content)

            # 执行AppleScript
            result = subprocess.run([
                'osascript', str(script_file)
            ], capture_output=True, text=True)

            if result.returncode != 0:
                logger.warning(f"配置DMG外观失败: {result.stderr}")
            else:
                logger.info("DMG外观配置完成")

        except Exception as e:
            logger.warning(f"配置DMG外观异常: {e}")

    def get_directory_size(self, directory: Path) -> int:
        """获取目录大小（MB）"""
        total_size = 0
        for item in directory.rglob('*'):
            if item.is_file():
                total_size += item.stat().st_size
        return total_size // (1024 * 1024)  # 转换为MB

    def create_mock_dmg(self, version: str, output_dir: Path = None) -> Dict[str, Any]:
        """
        创建模拟DMG（用于测试）

        Args:
            version: 版本号
            output_dir: 输出目录

        Returns:
            创建结果字典
        """
        logger.info("创建模拟DMG用于测试")

        if output_dir is None:
            output_dir = Path('dist')
        output_dir.mkdir(exist_ok=True)

        app_name = self.config['app_info']['name']
        dmg_name = f"{app_name}_v{version}_macOS.zip"  # 使用ZIP格式模拟DMG
        dmg_path = output_dir / dmg_name

        try:
            # 创建临时目录结构
            self.temp_dir = Path(tempfile.mkdtemp(prefix='mock_dmg_'))
            mock_app_dir = self.temp_dir / f"{app_name}.app"
            mock_app_dir.mkdir(parents=True)

            # 创建模拟的app结构
            contents_dir = mock_app_dir / 'Contents'
            contents_dir.mkdir()
            macos_dir = contents_dir / 'MacOS'
            macos_dir.mkdir()
            resources_dir = contents_dir / 'Resources'
            resources_dir.mkdir()

            # 创建Info.plist
            info_plist = f'''<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleExecutable</key>
    <string>{app_name}</string>
    <key>CFBundleIdentifier</key>
    <string>{self.config['app_info']['bundle_id']}</string>
    <key>CFBundleName</key>
    <string>{app_name}</string>
    <key>CFBundleDisplayName</key>
    <string>{self.config['app_info']['chinese_name']}</string>
    <key>CFBundleVersion</key>
    <string>{version}</string>
    <key>CFBundleShortVersionString</key>
    <string>{version}</string>
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>NSHighResolutionCapable</key>
    <true/>
</dict>
</plist>'''

            with open(contents_dir / 'Info.plist', 'w', encoding='utf-8') as f:
                f.write(info_plist)

            # 创建模拟可执行文件
            executable_path = macos_dir / app_name
            with open(executable_path, 'w') as f:
                f.write('#!/bin/bash\necho "Mock {app_name} v{version}"')
            executable_path.chmod(0o755)

            # 创建图标文件（简单文本文件模拟）
            icon_path = resources_dir / 'AppIcon.icns'
            with open(icon_path, 'w') as f:
                f.write('Mock icon file')

            # 创建README
            readme_content = self.create_readme_content()
            readme_path = self.temp_dir / 'README.txt'
            with open(readme_path, 'w', encoding='utf-8') as f:
                f.write(readme_content)

            # 创建Applications链接
            applications_link = self.temp_dir / 'Applications'
            applications_link.symlink_to('/Applications', target_is_directory=True)

            # 创建ZIP包
            import zipfile
            with zipfile.ZipFile(dmg_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
                for item in self.temp_dir.rglob('*'):
                    if item.is_file():
                        arcname = item.relative_to(self.temp_dir)
                        zipf.write(item, arcname)

            # 计算MD5
            import hashlib
            hash_md5 = hashlib.md5()
            with open(dmg_path, 'rb') as f:
                for chunk in iter(lambda: f.read(4096), b""):
                    hash_md5.update(chunk)

            file_size = dmg_path.stat().st_size

            logger.info(f"模拟DMG创建成功: {dmg_name}")
            logger.info(f"文件大小: {file_size / (1024 * 1024):.2f} MB")

            return {
                'success': True,
                'dmg_path': str(dmg_path),
                'dmg_name': dmg_name,
                'dmg_size': file_size,
                'dmg_size_mb': round(file_size / (1024 * 1024), 2),
                'md5': hash_md5.hexdigest(),
                'message': '模拟DMG创建成功'
            }

        except Exception as e:
            error_msg = f"创建模拟DMG失败: {e}"
            logger.error(error_msg)
            return {
                'success': False,
                'error': error_msg
            }
        finally:
            # 清理临时目录
            if self.temp_dir and self.temp_dir.exists():
                shutil.rmtree(self.temp_dir)

def main():
    """主函数"""
    import argparse

    parser = argparse.ArgumentParser(description='LimeReader macOS DMG创建工具')
    parser.add_argument('--config', default='release_config.json', help='配置文件路径')
    parser.add_argument('--app-path', help='应用程序路径（.app格式）')
    parser.add_argument('--version', required=True, help='版本号')
    parser.add_argument('--output-dir', help='输出目录')
    parser.add_argument('--mock', action='store_true', help='创建模拟DMG（用于测试）')

    args = parser.parse_args()

    try:
        creator = MacOSDMGCreator(args.config)

        if args.mock:
            # 创建模拟DMG
            result = creator.create_mock_dmg(args.version, Path(args.output_dir) if args.output_dir else None)
        else:
            # 创建真实DMG
            if not args.app_path:
                print("❌ 创建真实DMG需要提供 --app-path 参数")
                return 1

            app_path = Path(args.app_path)
            result = creator.create_dmg(app_path, args.version, Path(args.output_dir) if args.output_dir else None)

        if result['success']:
            print(f"✅ DMG创建成功!")
            print(f"文件路径: {result.get('dmg_path', 'N/A')}")
            print(f"文件大小: {result.get('dmg_size_mb', 0)} MB")
            print(f"MD5: {result.get('md5', 'N/A')}")
            return 0
        else:
            print(f"❌ DMG创建失败: {result.get('error')}")
            return 1

    except Exception as e:
        print(f"❌ 执行失败: {e}")
        return 1

if __name__ == '__main__':
    sys.exit(main())